<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        // call方法
        // call方法介绍
        // call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。
        // function.call(thisArg, arg1, arg2, ...)
        // thisArg可选的。在function函数运行时使用的this值。非严格模式下，指定为null或者undefined时会自动替换为指向全局对象。
        // 返回值
        // 使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值，则返回 undefined。
        // OK，下面来看一下示例
        var hunter = {
            value: 1
        }
        function hunterFn() {
            console.log(24, this.value)
        }
        hunterFn.call(hunter) // 1
        // 1, call()方法改变了this指向，指向了hunter
        // 2,hunterFn函数执行了。
        // 通过上面示例的总结，如何将this指向改变
        // 地球人都知道js中的方法，谁调用它，this就指向谁，通过这个特点，我们将上面的示例代码调整一下：
        var hunter = {
            value: 1,
            hunterFn: function () {
                console.log(34, this.value)
            }
        }
        hunter.hunterFn() // 1
        // 将hunterFn方法添加为hunter对象中的一个属性，这样调用hunterFn方法时，由于是hunter对象调用的hunterFn，因此this指向了hunter。这一步非常easy。
        // 这种方式有个很明显的缺点，改变this指向的时候，会平白无故的给hunter对象添加属性，这样搞肯定是不能滴，不过这个问题也很好解决，直接使用delete删除即可。
        // 所以，模拟call方法的步骤我们可以分为以下3步：
        //    1, 将hunterFn方法添加为hunter对象的属性
        //    2, 执行该函数
        //    3, 删除该函数
        // // 第一步
        // hunter.func = hunterFn
        // // 第二步
        // hunter.func()
        // // 第三步
        // delete hunter.func

        Function.prototype.MyCall = function (obj) {
            var context = obj
            context.func = this // 第一步
            context.func() // 第二步
            delete context.func // 第三步
        }
        var hunter = {
            value: 1
        }
        function hunterFn() {
            console.log(61, this.value)
        }
        hunterFn.MyCall(hunter) // 1
        // 上面这段代码做到了改变this指向，看起来是不是也很easy。
        // 文章的开头介绍call方法的时候，call方法还能够接受参数，并且接受的是一个参数列表(arg1, arg2, ...)
        // 接下来我们来搞定call方法接受参数这个问题
        // 由于call方法接受的参数是一个参数列表，并且个数还不是固定的，因此我们可以利用下面这个知识点来解决这个难题。
        // arguments是一个对应于传递给函数的参数的类数组对象。arguments对象是所有(非箭头)函数都可用的局部变量，你可以使用arguments对象在函数中引用函数的参数。此对象包含传递给函数的每个参数，第一个参数在索引0处。
        // 下面我们打印一下arguments对象中都有哪些内容
        Function.prototype.MyCall = function (obj) {
            console.log(71, arguments)
            // ...省略部分代码
        }
        hunterFn.MyCall(hunter, '森界降临', 27)
        // 通过上面的打印结果可以看出来，第0个参数为this指向的对象，后面的才是我们传进来的参数，因此我们可以从arguments对象中取出第二个到最后一个参数，放到一个数组中保存起来。由于arguments对象是一个类数组对象，我们可以使用for循环来搞定它。
        // 打印结果为1，不报错，到这里说明模拟call方法已经基本实现了，不过这并不是call方法的完整版，文章开头介绍call方法的时候，还有两点这段代码里没有实现，不过这都是小儿科了，分分钟搞定，剩下的两点如下：
        // 1,指定为null或者undefined时会自动替换为指向全局对象
        // 2,函数可以有返回值
        // 上面这两个问题都很好解决，具体的我直接在代码注释里解释一下，我们直接上最终版的代码吧！
        Function.prototype.MyCall = function (obj) {
            var context = obj || window // 问题一： 若obj不存在，则直接指向window
            var args = []
            for (var i = 1, len = arguments.length; i < len; i++) {
                args.push('arguments[' + i + ']')
            }
            context.func = this
            var result = eval('context.func(' + args + ')') // 保存执行结果
            delete context.func
            return result // 问题二：返回func方法的返回值
        }
        var hunter = {
            value: 1
        }
        var value = 666
        function hunterFn(name, age) {
            console.log(name, age, this.value)
        }
        hunterFn.MyCall(hunter, '森界降临', 27) // 森界降临 27 1
        hunterFn.MyCall(null, '森界降临', 27) // 森界降临 27 666
        hunterFn.MyCall(undefined, '森界降临', 27) // 森界降临 27 666
        // 上面的代码量看起来稍微有点多，如果可以使用ES6来实现的话，会简洁些：
        Function.prototype.MyCall = function (obj) {
            let context = obj || window
            let args = [...arguments].slice(1)  // 使用展开运算符处理arguments对象，转换为数组
            context.func = this
            var result = context.func(...args)
            delete context.func
            return result
        }
    </script>
</body>

</html>